<?php

require_once 'classes/strarray.cls.php';
require_once 'classes/tag.cls.php';

/**
 * The part of a form tied to a single value the user can set.
 */
interface FormInput {
	
	/**
	 * The name under which the form input is known to the user
	 */
	public function get_alias();
	
	/**
	 * Get the current value of the form input. This is used for running
	 * checks on the input. The value might only be sensible after 
	 * preprocessing has been performed.
	 */
	public function get_js_getter();
	
	/**
	 * preprocessing in javascript, before data validation. This must be an array of javascript code.
	 *  	It is allowed to abort sending. To archieve that, place 'return false' in the code.
	 */
	public function get_js_preprocessing();
	
	/**
	 * postprocessing in javascript, after the data validation. This must be an array of javascript code.
	 *      It is NOT allowed to abort sending with thic code. It is safe to assume that after 
	 *      postprocessing, the form will be sumbitted.
	 */
	public function get_js_postprocessing();
	
	/**
	 * Get the checks to run against the value of this input.
	 */
	public function get_checks();
	
	/**
	 * Set the default (initial) value.
	 */
	public function set_initial_value($value);
	
	/**
	 * Returns whether this Forminput claims an ActionInput.
	 */
	public function is_claiming();
	
	/**
	 * Name of the claimed input. Only relevant if is_claiming() is true, 
	 * otherwise this method returns null.
	 */
	public function get_claimed_input_name();
	
	/**
	 * The claimed Input.
	 */
	public function set_granted_input(Input $input);
	
	/**
	 * The claimed Input.
	 */
	public function get_granted_input();
}


/**
 * A part of a form or an entire form.
 */
abstract class FormComponent {
	
	private $id;
	/**
	 * A nice name, to show to the user should that be necessary. 
	 * E.g. "Order registration number" instead of "order_id" 
	 */
	private $alias;
	
	public function __construct() {
		$this->id = 'ID_' . rand(10000, 99999999); 
	}
	
	public function get_id() {
		return $this->id;
	}
	
	/**
	 * Get the alias. If not set, the id is used as alias.
	 */
	public function get_alias() {
		if ($this->alias === null) {
			$this->alias = $this->id;
		}
		return $this->alias;
	}
	
	public function set_alias($alias) {
		$this->alias = $alias;
	}
	
	/**
	 * Get all the form inputs.
	 */
	abstract public function get_form_inputs();
	
	/**
	 * Get all the form inputs that claim an input element
	 */
	abstract public function get_claiming_form_inputs();
	
	public function label($alias=null) {
		if ($alias != null) {
			$this->set_alias($alias);
		}		
		$labeled = new FormComposite();
		$labeled->add('label',  "\n<label>" . $this->get_alias(). "</label>\n");
		$labeled->add('main', $this);
		return $labeled;
	}
	
	abstract public function __toString();

}


/**
 * A component of the form that consists of sub-components.
 * 
 * Note that this might be a form input too (claiming or not). In case it is,
 * all FormInputs it contains are ignored when these are gathered. 
 */
class FormComposite extends FormComponent {
	
	// Components
	protected $c;
	
	public function __construct() {
		parent::__construct();
		$this->c = new StrArray();
	}
	
	public function add($key, $component) {
		if (isset($this->c[$key])) {
			throw new Exception("The key $key is already in use!");
		}
		$this->c[$key] = $component;
	}
	
	/**
	 * Get the the form inputs. If this object is a form input itself,
	 * return itself as the only form input.
	 * @see FormComponent::get_form_inputs()
	 */
	public function get_form_inputs() {
		if ($this instanceof FormInput) {
			return array($this);
		}
		else {
			$form_inputs = array();
			foreach ($this->c as $component) {
				if (!($component instanceof FormComponent)) {
					// Simple string or so, not part of the searchable tree
					continue;
				}
				$form_inputs = array_merge($form_inputs, $component->get_form_inputs());
			}
			return $form_inputs;
		}
	}
	
	/**
	 * Get the form inputs which are claiming an Action Input
	 * @see FormComponent::get_claiming_form_inputs()
	 */
	public function get_claiming_form_inputs() {
		$claiming_form_inputs = array();
		foreach ($this->get_form_inputs() as $form_input) {
			if ($form_input->is_claiming()) {
				$claiming_form_inputs[] = $form_input;
			}
		}
		return $claiming_form_inputs;
	}
	
	/**
	 * Wrap this formcomponent in a div
	 * @param string $class Class of the div. By default "formcomponent"
	 */
	public function wrap_in_div($class='formcomponent') {
		$glue = $this->c->get_glue();
		$this->c = $this->c->getArrayCopy();
		
		array_unshift($this->c, "<div class='$class'>");
		array_push($this->c, "</div>");
		
		$this->c = new StrArray($this->c);
		$this->c->set_glue($glue);
		
		return $this;
	}
	
	/**
	 * Return the tree of form components as a tree. This tree can be
	 * used to extract objects, and manipulate their properties.
	 */
	public function as_array() {
		$c = array();
		foreach ($this->c as $key => $comp) {
			$c[$key] = (method_exists($comp, 'as_array')) ? $comp->as_array(): $comp;
		}
		return $c;
	}
	
	public function __toString() {
		return $this->c->__toString();
	}
	
}


/**
 * A form component that does not consist of smaller components, 
 * and is a form input. Therefore it can have checks, pre- and postprocessing.
 */
abstract class FormInputLeaf extends FormComponent implements FormInput {
	
	private $input_name;
	protected $input;
	private $extra_checks;
	private $preprocessing;
	private $postprocessing;
	
	public function __construct($claimed_input_name=null) {
		parent::__construct();
		$this->input_name = $claimed_input_name;
		$this->input = null;
		$this->extra_checks = array();
		$this->preprocessing = array();
		$this->postprocessing = array();
	}
	
	public function is_claiming() {
		return $this->input_name !== null;	
	}
	
	public function get_claimed_input_name() {
		return $this->input_name;
	}
	
	public function set_granted_input(Input $input) {
		$this->input = $input;
		$this->set_alias($input->get_default_alias());
	}
	
	public function get_granted_input() {
		return $this->input;
	}
	
	public function get_form_inputs() {
		return array($this);
	}
	
	public function get_claiming_form_inputs() {
		if ($this->is_claiming()) {
			return array($this);
		}
		else {
			return array();
		}
	}
	
	/**
	 * Add check (used client-side only)
	 */
	public function add_check(ClientSideCheck $check) {
		$this->extra_checks[] = $check;
	}
	
	public function get_checks() {
		$checks = $this->extra_checks;
		if ($this->input) {
			$checks = array_merge($checks, $this->input->get_checks());
		}
		return $checks;
	}
	
	public function add_js_preprocessing($statement) {
		$this->preprocessing[] = $statement;
	}
	
	public function get_js_preprocessing() {
		return $this->preprocessing;
	}
	
	public function add_js_postprocessing($statement) {
		$this->postprocessing[] = $statement;
	}
	
	public function get_js_postprocessing() {
		return $this->postprocessing;
	}

	//abstract public function get_js_getter();
	//abstract public function set_initial_value();
	
}


class Form extends FormComposite {
	
	const TYPE_POST = 'POST';
	const TYPE_GET = 'GET';
	
	const CLOSED = 0;
	const OPEN = 1;
	const FINALIZED = 2;
	
	const INPUT_UNCLAIMED = 'UNCLAIMED';
	const INPUT_CLAIMED = 'CLAIMED';
	const INPUT_PRINTED = 'PRINTED';
	
	private $type;
	private $state;
	private $inputs = array();
	private $input_state = array();

	public function __construct($inputs, $type=Form::TYPE_POST) {
		parent::__construct();
		$this->state = Form::CLOSED;
		$this->type = $type;
		$this->inputs = $inputs; 
		foreach (array_keys($inputs) as $input_name) {
			$this->input_state[$input_name] = FORM::INPUT_UNCLAIMED;
		}
	}
	
	public function set_type($type) {
		$this->type = $type;
	}
	
	public function get_js_submitter() {
		return "\$('#{$this->get_id()}').submit";
	}
	
	/*public function js_elem() {
		return "\$('#{$this->get_id()}')";
	}*/
	
	/*public function get_input($name) {
		return $this->inputs[$name];
	}*/
	
	public function start() {
		assert($this->state == Form::CLOSED);
		$this->state = Form::OPEN;
		
		$action = $this->generate(new HiddenField(
 			Action::INPUT_ACTION, 
 			$this->inputs[Action::INPUT_ACTION]->get_value()
		));
		
		$id = $this->get_id();
		return "<form id='$id' action='index.php' method='{$this->type}' onSubmit='return check_form_$id();'>
					" . $action->__toString();
			 
	}
	
	/**
	 * Generate a new component of the form. This registers the leafs
	 * the component claims. Afterwards, the component is returned again.
	 * @param $composite
	 */
	public function generate($component) {
		if ($this->state != Form::OPEN) {
			throw new Exception('You forgot to open the form by $form->start()!');
		}
		foreach ($component->get_claiming_form_inputs() as $form_inputs) {
			$input_name = $form_inputs->get_claimed_input_name();
			if (!isset($this->inputs[$input_name])) {
				throw new Exception("Name $input_name is not an input of this form!");
			}
			if ($this->input_state[$input_name] != Form::INPUT_UNCLAIMED) {
				throw new Exception("Name $input_name has already been claimed!");
			}
			$this->input_state[$input_name] = Form::INPUT_CLAIMED;
			$form_inputs->set_granted_input($this->inputs[$input_name]);
		}
		$this->c[] = $component;
		return $component;
	}
	
	/**
	 * Mark an input printed. This is for exotic cases where
	 * you can not echo the form element bound to an input 
	 * through the form.
	 * @param string $input_name
	 */
	public function mark_bound($input_name) {
		if (!isset($this->inputs[$input_name])) {
			throw new Exception("Name $input_name is not an input of this form!");
		}
		$this->input_state[$input_name] = Form::INPUT_PRINTED;
	}
	
	// TODO: enable
	/*public function out(FormComponent $component) {
		foreach ($component->get_claiming_form_inputs() as $form_input) {
			$this->input_state[$form_input->get_claimed_input_name()] = Form::INPUT_PRINTED;
		}
		echo $component;
	}*/
	
	public function generate_submit($text='Send') {
		return new SubmitButton($text);
	}
	
	public function end() {
		assert($this->state == Form::OPEN);
		$this->state = Form::FINALIZED;
		// Make sure that all inputs were registered for
		foreach ($this->input_state as $name => $state) {
			/*if ($state != Form::INPUT_PRINTED) {
				if ($state == FORM::INPUT_CLAIMED) {
					throw new Exception("Input claimed but not printed: $name");
				}
				else {
					throw new Exception("Input not claimed: $name");
				}
			}*/
			if ($state == Form::INPUT_UNCLAIMED) {
				throw new Exception("Input not claimed: $name");
			}
		}
		return '</form>' . $this->generate_send_script();
	}
	
	/**
	 * Get the ending tag of the form and the javascript that checks the 
	 * form on submit.
	 */
	public function generate_send_script() {
		
		$form_inputs = $this->get_form_inputs();	
		
		$script = "
			<script type='text/javascript'><!--
				
				function check_form_{$this->get_id()}() {";
		
		foreach ($form_inputs as $form_input) {
			foreach ($form_input->get_js_preprocessing() as $statement) {
				$script .= "
						$statement
				";
			}
		}
		
		
		foreach ($form_inputs as $form_input) {
			foreach ($form_input->get_checks() as $check) {
				assert($check instanceof ClientSideCheck);
				$js = trim($check->validate_js('value'));
				if ($js == 'return true;') {
					continue;
				}
				$getter = $form_input->get_js_getter();
				$value = $getter . '()';
				if ($form_input->is_claiming() && ($form_input->get_granted_input() instanceof JsonInput)) {
					$value = "JSON.parse($value)";
				}
				$comment = addslashes($check->comment($form_input->get_alias()));
				$id = rand(10000, 9999999);
				// TODO: make check method static or so, include the checking code
				// only once. 
				$script .= "
					var check_$id = function (value) {
						$js
					};
					if ( ! check_$id($value) ) {
						alert('$comment');
						return false;
					}
				";
			}
		}		
		
		foreach ($form_inputs as $form_input) {
			foreach ($form_input->get_js_postprocessing() as $statement) {
				$script .= "
					$statement
				";
			}
		}
		
		// All ok.
		$script .= "
					return true;
				}

		   	//--></script>
		";
		
		return $script;
	}
	
}


/**
 * A FormInput that consists of a single tag (open and perhaps closing tag).
 */
abstract class SingleTagFormInput extends FormInputLeaf {
 	
 	/**
 	 * Attributes and properties to be added to the input tag.
 	 */
	protected $tag;
	
	public function __construct($tag_name, $claimed_input_name=null, $value=null) {
		parent::__construct($claimed_input_name);
		$this->tag = new Tag($tag_name);
		if ($value) {
			$this->set_initial_value($value);
		}
	}
	
	public function get_id() {
		return $this->is_claiming() ? $this->get_claimed_input_name() . '_' . parent::get_id() : parent::get_id();
	}
	
	public function get_js_elem() {
		return "\$('#{$this->get_id()}')";
	}

	public function get_js_setter() {
		return $this->get_js_elem() . '.val';
	}
	
	public function get_js_getter() {
		return $this->get_js_elem() . '.val';
	}
	
	public function set_attr($attr, $value) {
		$this->tag->set_attr($attr, $value);
	}
	
	public function get_attr($attr) {
		return $this->tag->get_attr($attr);
	}
	
	public function add_prop($prop) {
		$this->tag->add_prop($prop);
	}
	
	public function remove_prop($prop) {
		$this->tag->remove_prop($prop);
	}
	
	public function add_class($class) {
		$this->tag->add_class($class);
	}
	
	public function __toString() {
		$this->tag->set_attr("id", $this->get_id());
		if ($this->is_claiming()) {
			$this->tag->set_attr("name", $this->get_claimed_input_name());
		}
		return $this->tag->__toString();
	}
	
	//abstract public function set_initial_value();
	//abstract public function get_initial_value();
	
}


/**
 * An InputTag is a FormInput that consists of one <input> field. 
 * The tag gets the "<type>-input" CSS class automatically.
 */
class InputTag extends SingleTagFormInput {
	
	/**
	 * HTML input tag type attribute. Can be overridden by subclasses.
	 */
	protected $type = "text"; // Default
	
	public function __construct($claimed_input_name=null, $value=null) {
		parent::__construct('input', $claimed_input_name, $value);
		$this->tag->set_attr("type", $this->type);
		$this->add_class($this->type . '-input');
	}
	
	public function set_initial_value($value) {
		$this->tag->set_attr('value', $value);
	}
	
	// For convenience
	public function get_initial_value() {
		return $this->tag->get_attr('value');
	}
	
}


/**
 * A base class that is a form composite, but implements FormInput.
 * All FormInputLeaf methods are redirected to a "main" component.
 * 
 * $main should be the input claiming an input.
 */
class ExtendedFormInputLeaf extends FormComposite implements FormInput {
	
	private $main;
	
	public function __construct(FormInputLeaf $main, $key='main') {
		parent::__construct();
		$this->main = $main;
		$this->c[$key] = $main;
	}
	
	public function get_main() {
		return $this->main;
	}
	
	public function get_alias() {
		return $this->main->get_alias();
	}
	
	public function set_alias($alias) {
		return $this->main->set_alias($alias);
	}
	
	public function get_js_getter() {
		return $this->main->get_js_setter();
	}
	
	// Use is unclear
	public function get_js_setter() {
		return $this->main->get_js_setter();
	}
	
	public function set_initial_value($value) {
		$this->main->set_initial_value($value);
	}
	
	public function add_js_preprocessing($statement) {
		$this->main->add_js_preprocessing($statement);
	}
	
	public function get_js_preprocessing() {
		return $this->main->get_js_preprocessing();
	}
	
	public function add_js_postprocessing($statement) {
		$this->main->add_js_postprocessing($statement);
	}

	public function get_js_postprocessing() {
		return $this->main->get_js_postprocessing();
	}
	
	public function get_checks() {
		return $this->main->get_checks();
	}
	
	public function add_check(ClientSideCheck $check) {
		return $this->main->add_check($check);
	}
	
	public function is_claiming() {
		return $this->main->is_claiming();
	}
	
	public function get_claimed_input_name() {
		return $this->main->get_claimed_input_name();
	}
	
	public function set_granted_input(Input $input) {
		$this->main->set_granted_input($input);
	}
	
	public function get_granted_input() {
		return $this->main->get_granted_input();
	}
	
}


abstract class DoubleTagFormInput extends ExtendedFormInputLeaf {
	
	private $visible;
	
	public function __construct(FormInput $visible, FormInput $main, $key_visible='visible', $key_main='hidden') {
		parent::__construct($main, $key_main);
		$this->visible = $visible;
		$this->c[$key_visible] = $visible;
	}
	
	public function get_visible() {
		return $this->visible;
	}

	// Use unclear
	public function get_js_setter() {
		return $this->get_main()->get_js_setter();
	}
	
	public function get_js_getter() {
		return $this->get_main()->get_js_getter();
	}
	
	public function set_attr($attr, $value) {
		$this->get_main()->set_attr($attr, $value);
	}
	
	public function get_attr($attr) {
		return $this->get_main()->get_attr($attr);
	}
	
	public function set_initial_value($value) {
		$this->get_visible()->set_initial_value($value);
	}
	
	public function get_initial_value() {
		$this->get_visible()->get_initial_value();
	}
	
	// TODO: only available if visible supports it
	public function add_prop($prop) {
		$this->get_visible()->add_prop($prop);
	}
	
	// TODO: only available if visible supports it
	public function remove_prop($prop) {
		$this->get_visible()->remove_prop($prop);
	}
	
	// TODO: only available if visible supports it
	public function add_class($class) {
		$this->get_visible()->add_class($class);
	}	
	
}